package edu.udo.sopra10.chaturaji.entities;

import static edu.udo.sopra10.chaturaji.util.ExceptionUtil.assertInRange;
import static edu.udo.sopra10.chaturaji.util.ExceptionUtil.assertNotNull;
import static edu.udo.sopra10.chaturaji.util.ExceptionUtil.assertNotNullOrContainsNull;
import static java.lang.Byte.MAX_VALUE;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableCollection;
import static org.apache.commons.lang.builder.ToStringStyle.SHORT_PREFIX_STYLE;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;

import org.apache.commons.lang.builder.ToStringBuilder;

import edu.udo.sopra10.chaturaji.entities.coordinates.Position;
import edu.udo.sopra10.chaturaji.entities.pieces.Pawn;
import edu.udo.sopra10.chaturaji.entities.pieces.Piece;

/**
 * A move of a {@link Piece}. This is the typical type of {@link Move}. This
 * contains information of the piece's movement on the board as well as capture
 * pieces and other turn consequences.
 * 
 * @author Simon Dierl
 */
public final class PieceMove extends Move {
	/**
	 * The pieces that was captured by the moving piece. Includes triumphed
	 * boats. May be an {@link Collections#EMPTY_LIST} if the turn was no boat
	 * triumph to save memory.
	 */
	private Collection<Piece> capturedPieces;

	/**
	 * The position to which the moving piece was moved.
	 */
	private Position endPosition;

	/**
	 * The piece that was moved.
	 */
	private Piece movedPiece;

	/**
	 * The pieces to which pawns was promoted as a consequence of the move. May
	 * be an {@link Collections#EMPTY_LIST} if the turn did not cause pawn
	 * promotions to save memory.
	 */
	private Collection<Piece> promotedPawns;

	/**
	 * The pawns which were promoted as a consequence of the move. May be an
	 * {@link Collections#EMPTY_LIST} if the turn did not cause pawn promotions
	 * to save memory.
	 */
	private Collection<Pawn> promotionSourcePawns;

	/**
	 * The score gained by the {@link Player} in this move.
	 */
	private byte score;

	/**
	 * The position from which the moving piece was moved.
	 */
	private Position startPosition;

	/**
	 * The {@link Turn} to which this move belongs.
	 */
	private Turn turn;

	/**
	 * Create a new piece move with the given parameters.
	 * 
	 * @param turn
	 *            the turn in which the move was made, must not be {@code null}.
	 * @param diceRoll
	 *            the result of the dice roll that allowed this move, must be in
	 *            {@code [1, 6]}.
	 * @param startPosition
	 *            the position from which the moved piece originated, must not
	 *            be {@code null}.
	 * @param endPosition
	 *            the position on which the moved piece was set down, must not
	 *            be {@code null}.
	 * @param movedPiece
	 *            the piece that was moved, must not be {@code null}.
	 * @param score
	 *            the score gained by the player as a result of the move, must
	 *            be {@code >= 0}.
	 * @param capturedPieces
	 *            the pieces, if any, that were captured by this move,
	 *            {@code null} is equivalent to an empty collection.
	 * @param promotionSourcePawns
	 *            the {@link Pawn}s that sourced promotions (the pawns
	 *            themselves) as a consequence of this move. {@code null} is
	 *            equivalent to an empty collection.
	 * @param promotedPawns
	 *            the pawn promotions (the resulting pieces) that were effected
	 *            by the move. {@code null} is equivalent to an empty
	 *            collection.
	 */
	public PieceMove(final Turn turn, final byte diceRoll,
			final Position startPosition, final Position endPosition,
			final Piece movedPiece, final byte score,
			final Collection<Piece> capturedPieces,
			final Collection<Pawn> promotionSourcePawns,
			final Collection<Piece> promotedPawns) {
		super(diceRoll);
		setTurn(turn);
		setStartPosition(startPosition);
		setEndPosition(endPosition);
		setMovedPiece(movedPiece);
		setScore(score);
		if ((promotedPawns == null) || promotedPawns.isEmpty()) {
			this.promotedPawns = emptyList();
		} else {
			this.promotedPawns = new ArrayList<Piece>(promotedPawns);
		}
		if ((promotionSourcePawns == null) || promotionSourcePawns.isEmpty()) {
			this.promotionSourcePawns = emptyList();
		} else {
			this.promotionSourcePawns = new ArrayList<Pawn>(
					promotionSourcePawns);
		}
		if ((capturedPieces == null) || capturedPieces.isEmpty()) {
			this.capturedPieces = emptyList();
		} else {
			this.capturedPieces = new ArrayList<Piece>(capturedPieces);
		}
	}

	/**
	 * Add a captured {@link Piece} to the collection of captured pieces.
	 * 
	 * @param capturedPiece
	 *            the new captured piece, must not be {@code null}.
	 * @return if the piece was new to the collection. Always returns
	 *         {@code true}.
	 */
	public boolean addCapturedPiece(final Piece capturedPiece) {
		assertNotNull(capturedPiece, "captured piece");
		if (capturedPieces.isEmpty()) {
			capturedPieces = new ArrayList<Piece>(4);
		}
		return capturedPieces.add(capturedPiece);
	}

	/**
	 * Add several captured {@link Piece}s to the collection of captured pieces.
	 * 
	 * @param capturedPieces
	 *            the new captured pieces, must not be {@code null} or contain
	 *            {code null}. This collection is copied to preserve
	 *            encapsulation.
	 * @return if one if the pieces was new to the collection. Always returns
	 *         {@code !capturedPieces.isEmpty()}.
	 */
	public boolean addCapturedPieces(final Collection<Piece> capturedPieces) {
		if (assertNotNullOrContainsNull(capturedPieces, "captured pieces")
				.isEmpty()) {
			return false;
		}
		if (this.capturedPieces.isEmpty()) {
			this.capturedPieces = new ArrayList<Piece>(capturedPieces);
			return true;
		}
		return this.capturedPieces.addAll(capturedPieces);
	}

	/**
	 * Add a promoted {@link Pawn} (the result of promotion) to the collection
	 * of promoted pawns.
	 * 
	 * @param promotedPawn
	 *            the new promoted pawn, must not be {@code null}.
	 * @return if the piece was new to the collection. Always returns
	 *         {@code true}.
	 */
	public boolean addPromotedPawn(final Piece promotedPawn) {
		assertNotNull(promotedPawn, "promoted pawn");
		if (promotedPawns.isEmpty()) {
			promotedPawns = new ArrayList<Piece>(4);
		}
		return promotedPawns.add(promotedPawn);
	}

	/**
	 * Add several promoted {@link Pawn}s (the results of promotion) to the
	 * collection of promoted pawns.
	 * 
	 * @param promotedPawns
	 *            the new promoted pawn, must not be {@code null}.
	 * @return if the piece was new to the collection. Always returns
	 *         {@code true}.
	 */
	public boolean addPromotedPawns(final Collection<Piece> promotedPawns) {
		if (assertNotNullOrContainsNull(promotedPawns, "promoted pawns")
				.isEmpty()) {
			return false;
		}
		if (this.promotedPawns.isEmpty()) {
			this.promotedPawns = new ArrayList<Piece>(promotedPawns);
			return true;
		}
		return this.promotedPawns.addAll(promotedPawns);
	}

	/**
	 * Add a promotion source {@link Pawn} (the pawn itself) to the collection
	 * of promotion source pawns.
	 * 
	 * @param promotionSourcePawn
	 *            the new promotion source pawn, must not be {@code null}.
	 * @return if the pawn was new to the collection. Always returns
	 *         {@code true}.
	 */
	public boolean addPromotionSourcePawn(final Pawn promotionSourcePawn) {
		assertNotNull(promotionSourcePawn, "promotion source pawn");
		if (promotionSourcePawns.isEmpty()) {
			promotionSourcePawns = new ArrayList<Pawn>(4);
		}
		return promotionSourcePawns.add(promotionSourcePawn);
	}

	/**
	 * Add several promotion source {@link Pawn}s (the pawns themselves) to the
	 * collection of promotion source pawns.
	 * 
	 * @param promotionSourcePawns
	 *            the new promotion source pawn, must not be {@code null}.
	 * @return if the piece was new to the collection. Always returns
	 *         {@code true}.
	 */
	public boolean addPromotionSourcePawns(
			final Collection<Pawn> promotionSourcePawns) {
		if (assertNotNullOrContainsNull(promotionSourcePawns,
				"promotion source pawns").isEmpty()) {
			return false;
		}
		if (this.promotionSourcePawns.isEmpty()) {
			this.promotionSourcePawns = new ArrayList<Pawn>(
					promotionSourcePawns);
			return true;
		}
		return this.promotionSourcePawns.addAll(promotionSourcePawns);
	}

	/**
	 * Modify the score gained by the {@link Player} because of this move by a
	 * given value.
	 * 
	 * @param score
	 *            the modification to add to the score. May be negative to
	 *            decrement the score, but must not reduce the score to a value
	 *            {@code < 0}.
	 * @return the new score.
	 */
	public byte alterScore(final byte score) {
		setScore((byte) (this.score + score));
		return this.score;
	}

	/**
	 * Remove all captured {@link Piece}s from the collection of captured
	 * pieces.
	 */
	public void clearCapturedPieces() {
		capturedPieces.clear();
	}

	/**
	 * Remove all promoted {@link Pawn}s (the results of promotion) from the
	 * collection of promoted pawns.
	 */
	public void clearPromotedPawns() {
		promotedPawns.clear();
	}

	/**
	 * Remove all promotion source {@link Pawn}s (the pawns themselves) from the
	 * collection of promotion source pawns.
	 */
	public void clearPromotionSourcePawns() {
		promotionSourcePawns.clear();
	}

	/**
	 * Get the pieces that were captured because of a by this move, including
	 * boat triumphs.
	 * 
	 * @return the captured pieces, never {@code null}, never contains
	 *         {@code null}, always unmodifiable.
	 */
	public Collection<Piece> getCapturedPieces() {
		return unmodifiableCollection(capturedPieces);
	}

	/**
	 * Get the {@link Position} on which this move ended.
	 * 
	 * @return the end position of this move, never {@code null}.
	 */
	public Position getEndPosition() {
		return endPosition;
	}

	/**
	 * Get the {@link Piece} that was moved.
	 * 
	 * @return the moved piece, never {@code null}.
	 */
	public Piece getMovedPiece() {
		return movedPiece;
	}

	/**
	 * Get the {@link Pawn}s that were promoted (the results of promotion) as a
	 * consequence of this move.
	 * 
	 * @return the promoted pawns, never {@code null}, never contains
	 *         {@code null}, always unmodifiable.
	 */
	public Collection<Piece> getPromotedPawns() {
		return unmodifiableCollection(promotedPawns);
	}

	/**
	 * Get the {@link Pawn}s that sourced promotions (the pawns themselves) as a
	 * consequence of this move.
	 * 
	 * @return the promotion source pawns, never {@code null}, never contains
	 *         {@code null}, always unmodifiable.
	 */
	public Collection<Pawn> getPromotionSourcePawns() {
		return unmodifiableCollection(promotionSourcePawns);
	}

	/**
	 * Get the score gained by the {@link Player} because of this move.
	 * 
	 * @return the gained score, always {@code >= 0}.
	 */
	public byte getScore() {
		return score;
	}

	/**
	 * Get the {@link Position} on which this move started.
	 * 
	 * @return the start position of this move, never {@code null}.
	 */
	public Position getStartPosition() {
		return startPosition;
	}

	/**
	 * Get the turn in which this move was made,
	 * 
	 * @return the containing turn, never {@code null}.
	 */
	public Turn getTurn() {
		return turn;
	}

	/**
	 * Remove a captured {@link Piece} from the collection of captured pieces.
	 * 
	 * @param capturedPiece
	 *            the captured piece to remove, must not be {@code null}.
	 * @return if the piece was contained in the collection.
	 */
	public boolean removeCapturedPiece(final Piece capturedPiece) {
		assertNotNull(capturedPiece, "captured piece");
		if (capturedPieces.isEmpty()) {
			return false;
		}
		if (capturedPieces.remove(capturedPiece)) {
			if (capturedPieces.isEmpty()) {
				capturedPieces = emptyList();
			}
			return true;
		}
		return false;
	}

	/**
	 * Remove several captured {@link Piece}s from the collection of promoted
	 * pawns.
	 * 
	 * @param capturedPieces
	 *            the captured pieces to remove, must not be {@code null}. May
	 *            contain {@code null}, although this has no effect and is
	 *            discouraged.
	 * @return if any of the pieces were contained in the collection.
	 */
	public boolean removeCapturedPieces(Collection<Piece> capturedPieces) {
		if (assertNotNull(capturedPieces, "captured pieces").isEmpty()
				|| this.capturedPieces.isEmpty()) {
			return false;
		}
		if (this.capturedPieces.removeAll(capturedPieces)) {
			if (this.capturedPieces.isEmpty()) {
				capturedPieces = emptyList();
			}
			return true;
		}
		return false;
	}

	/**
	 * Remove a promoted {@link Pawn} (the result of promotion) from the
	 * collection of promoted pawns.
	 * 
	 * @param promotedPawn
	 *            the promoted pawn to remove, must not be {@code null}.
	 * @return if the piece was contained in the collection.
	 */
	public boolean removePromotedPawn(final Piece promotedPawn) {
		assertNotNull(promotedPawn, "promoted pawn");
		if (promotedPawns.isEmpty()) {
			return false;
		}
		if (promotedPawns.remove(promotedPawn)) {
			if (promotedPawns.isEmpty()) {
				promotedPawns = emptyList();
			}
			return true;
		}
		return false;
	}

	/**
	 * Remove several promoted {@link Pawn}s (the results of promotion) from the
	 * collection of promoted pawns.
	 * 
	 * @param promotedPawns
	 *            the promoted pawns to remove, must not be {@code null}. May
	 *            contain {@code null}, although this has no effect and is
	 *            discouraged.
	 * @return if any of the pieces were contained in the collection.
	 */
	public boolean removePromotedPawns(final Collection<Piece> promotedPawns) {
		if (assertNotNull(promotedPawns, "promoted pawns").isEmpty()
				|| this.promotedPawns.isEmpty()) {
			return false;
		}
		if (this.promotedPawns.removeAll(promotedPawns)) {
			if (this.promotedPawns.isEmpty()) {
				this.promotedPawns = emptyList();
			}
			return true;
		}
		return false;
	}

	/**
	 * Remove a promotion source {@link Pawn} (the pawn itself) from the
	 * collection of promotion source pawns.
	 * 
	 * @param promotionSourcePawn
	 *            the promotion source pawn to remove, must not be {@code null}.
	 * @return if the pawn was contained in the collection.
	 */
	public boolean removePromotionSourcePawn(final Pawn promotionSourcePawn) {
		assertNotNull(promotionSourcePawn, "promotion source pawn");
		if (promotionSourcePawns.isEmpty()) {
			return false;
		}
		if (promotionSourcePawns.remove(promotionSourcePawn)) {
			if (promotionSourcePawns.isEmpty()) {
				promotionSourcePawns = emptyList();
			}
			return true;
		}
		return false;
	}

	/**
	 * Remove several promotion source {@link Pawn}s (the pawns themselves) from
	 * the collection of promotion source pawns.
	 * 
	 * @param promotionSourcePawns
	 *            the promotion source pawns to remove, must not be {@code null}
	 *            . May contain {@code null}, although this has no effect and is
	 *            discouraged.
	 * @return if any of the pawns were contained in the collection.
	 */
	public boolean removePromotionSourcePawns(
			final Collection<Pawn> promotionSourcePawns) {
		if (assertNotNull(promotionSourcePawns, "promotion source pawns")
				.isEmpty() || this.promotionSourcePawns.isEmpty()) {
			return false;
		}
		if (this.promotionSourcePawns.removeAll(promotionSourcePawns)) {
			if (this.promotionSourcePawns.isEmpty()) {
				this.promotionSourcePawns = emptyList();
			}
			return true;
		}
		return false;
	}

	/**
	 * Set the {@link Position} on which this move ended.
	 * 
	 * @param endPosition
	 *            the new end position of this move, must not be {@code null}.
	 */
	public void setEndPosition(final Position endPosition) {
		this.endPosition = assertNotNull(endPosition, "end position");
	}

	/**
	 * Set the {@link Piece} that was moved.
	 * 
	 * @param movedPiece
	 *            the new moved piece, must not be {@code null}.
	 */
	public void setMovedPiece(final Piece movedPiece) {
		this.movedPiece = assertNotNull(movedPiece, "moved piece");
	}

	/**
	 * Set the score gained by the {@link Player} because of this move.
	 * 
	 * @param score
	 *            the new gained score, must be {@code >= 0}.
	 */
	public void setScore(final byte score) {
		this.score = assertInRange(score, (byte) 0, MAX_VALUE, "score");
	}

	/**
	 * Set the {@link Position} on which this move started.
	 * 
	 * @param startPosition
	 *            the new start position of this move, must not be {@code null}.
	 */
	public void setStartPosition(final Position startPosition) {
		this.startPosition = assertNotNull(startPosition, "start position");
	}

	/**
	 * Set the turn in which the move was made.
	 * 
	 * @param turn
	 *            the new containing turn, must not be {@code null}.
	 */
	public void setTurn(final Turn turn) {
		this.turn = assertNotNull(turn, "turn");
	}

	@Override
	public String toString() {
		return new ToStringBuilder(this, SHORT_PREFIX_STYLE)
				.append("turn", turn)
				.append("diceRoll", diceRoll)
				.append("startPosition", startPosition)
				.append("endPosition", endPosition)
				.append("movedPieceType", movedPiece.getClass().getSimpleName())
				.append("score", score).toString();
	}
}
